from queue import Empty
from multiprocessing import Process, Queue, current_process, ProcessError, Pipe, Manager
from .util import *
from abc import *
import traceback
from pickle import dumps, loads
import types

class Simulation_Message:
	"""Base class of messages passed around on the interprocess queues."""
	
class Simulation_Kill_Message (Simulation_Message):
	"""A process (clean) kill message."""

	def execute( self, simulation ):
		simulation.alive = False

class Simulation_Event_Message_Command:
	def __init__( self, fun, *args, **kwargs ):
		self.fun = fun
		self.args = args
		self.kwargs = kwargs

class Simulation_Event_Message (Simulation_Message):
	"""This message contains an event to be executed."""
	__slots__ = ['obj','cmds','time']
	
	def __init__( self, time, obj, *cmds ):
		"""Create an event."""
		self.time = time
		self.obj = obj
		self.cmds = list( cmds )
		
		for c in cmds:
			if not isinstance( c, Simulation_Event_Message_Command ):
				raise ValueError( "Arguments must instances of Simulation_Event_Message_Command." )
	
	def execute( self, simulation ):
		simulation.queue.push( (self.time, self) )
	
	def process( self ):
		"""Execute an event."""
		obj = self.obj.resolve( )
		
		try:
			for c in self.cmds:
				if c.fun == 'getattr':
					obj = getattr( obj, c.args[0] )
				
				elif c.fun == 'setattr':
					setattr( obj, c.args[0], c.args[1] )
				
				elif c.fun == 'call':
					if c.kwargs != None and len( c.kwargs ) > 0:
						obj( *c.args, **c.kwargs )
					else:
						obj( *c.args )
						
		except:
			traceback.print_exc( )

class Simulation (Process):
	"""Parallel DES process class."""
	__slots__ = ['queue','extqueue','time','alive','running','links','object','children','sim_id']

	manager = None
	obj_map = None

	@classmethod
	def get_simulation( self ):
		return current_process( )
	
	def __init__( self, 
			*objects : "A list of objects contained in the simulation." ):
		"""Constructor for a simulation thread."""
		Process.__init__( self )
		
		self.queue = Priority_Queue( )
		self.extqueue = Simulation.manager.Queue( )

		self.time = 0
		self.alive = True
		self.running = True
		self.children = []
		self.parent_pipe, self.child_pipe = Pipe( )
		self.done = False
		self.num_events = 0
		self.objects = {}

		i = 1
		for o in objects:
			if not isinstance( o, Simulation_Object ):
				raise ValueError( "Argument %d must be an instance of Simulation_Object." % i )
			self.add_object( o )
			i += 1
			
		self.start( )
		
	def add_object( self, obj ):
		"""Add an object to a simulation."""
		if not isinstance( obj, Simulation_Object ):
			raise ValueError( "Tried to add a non-simulatable object." )

		if obj._sim_id not in Simulation.obj_map:
			Simulation.obj_map[obj._sim_id] = dumps( self.extqueue )
			self.objects[obj._sim_id] = obj

		else:
			raise ValueError( "This object is already in another simulation." )

	def remove_object( self, obj ):
		"""
		Remove an object from the simulation. If called from another process
		returns the object to the calling process.
		"""
		if Simulation.get_simulation( ) != self:
			self.parent_pipe.send( obj._sim_id )
			return self.parent_pipe.recv( )

	def kill( self ):
		"""Kills simulations recursively soon as possible."""
		self.extqueue.put( dumps( Simulation_Kill_Message( ) ) )

	@staticmethod
	def schedule( *events : "Simulation_Message or Simulation_Event_Catcher objects to schedule." ):
		"""Schedule an event to occur at a certain time."""
		for event in events:
			if isinstance( event, Simulation_Event_Catcher ):
				event = event._event
			
			if not isinstance( event, Simulation_Message ):
				raise ValueError( "Parameter 'event' must be a Simulation_Message or Simulation_Event_Catcher instance." )

			#Simulation.obj_map[event.obj._sim_id].put( event )
			queue = loads( Simulation.obj_map[event.obj._sim_id] )
			queue.put( dumps( event ) )
		
	def run( self ):
		"""Thread function that does the actual simulation."""
		queue = self.queue
		extqueue = self.extqueue
		while self.alive:
			while not self.running and self.alive:
				pass

			### Check the external queue for incoming messages. ###

			try:
				q = extqueue.get( False )
				
				if q == None:
					pass
				
				else:
					q = loads( q )
					if isinstance( q, Simulation_Message ):
						q.execute( self )
					
				
			except Empty:
				pass
					
			except IOError:
				self.alive = False
				break
			
			except:
				traceback.print_exc( )

			### Check the event queue for the next scheduled event. ###

			q = queue.peek( )
		
			if q != None:
				time, event = q
				if time >= self.time:
					self.time = time
					event.process( )
				queue.pop( )
			
			### Check the parent pipe for objects to remove. ###
			
			while self.child_pipe.poll( 0.01 ):
				sim_id = self.child_pipe.recv( )

				obj = self.objects[sim_id]
				del self.objects[sim_id]
				obj._sim_id = -1
					
				self.child_pipe.send( obj )

		self.done = True

class Simulation_Object:
	"""Base class of simulatable objects."""
	__slots__ = ['_sim_id']
	
	def __init__( self ):
		self._sim_id = id( self )

	def __getstate__( self ):
		if self._sim_id > -1:
			return {'_sim_id' : self._sim_id}
		else:
			d = {'_sim_id' : self._sim_id}
			d.update( self.__dict__ )
			return d
		
	def __setstate__( self, state ):
		if state['_sim_id'] > -1:
			self._sim_id = state['_sim_id']
		else:
			self.__dict__.update( state )

	def resolve( self ):
		return Simulation.get_simulation( ).objects[self._sim_id]

class Simulation_Event_Catcher:
	__slots__ = ['_event', '_autoschedule']
	
	def __init__( self, obj, time, **kwargs ):
		self._event = Simulation_Event_Message( obj, time )
		autosched = True
		if 'autoschedule' in kwargs:
			autosched = kwargs['autoschedule']
		self._autoschedule = autosched
			
	def __getattr__( self, attr ):
		if attr in ['_event', '_autoschedule', '__dict__']:
			return object.__getattr__( self, attr )
		else:
			self._event.cmds.append( 
				Simulation_Event_Message_Command( 'getattr', attr ) )
			return self
	
	def __setattr__( self, attr, value ):
		if attr in ['_event', '_autoschedule']:
			object.__setattr__( self, attr, value )
			
		else:
			self._event.cmds.append( 
				Simulation_Event_Message_Command( 'setattr', attr, value ) )
			if self._autoschedule:
				Simulation.schedule( self._event )
				self._event = None
				
		return self

	def __call__( self, *args, **kwargs ):
		self._event.cmds.append( 
			Simulation_Event_Message_Command( 'call', *args, **kwargs ) )

		if self._autoschedule:
			Simulation.schedule( self._event )
			self._event = None
	
		return self
	
def future( obj, time, **kwargs ):
	return Simulation_Event_Catcher( time, obj, **kwargs )

def schedule( *events ):
	Simulation.schedule( *events )

def get_time( ):
	return Simulation.get_simulation( ).time	
